﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace Yappi
{
    /// <summary>
    /// Provides fast access to virtual property implementations by declaring type, implementing type and property name.
    /// All calls to virtual methods done as call (not callvirt) giving access to exact implementation of property.
    /// All operations in this class is O(1) except first access wich takes O(N) where N is number of properties in TType.
    /// All methods a thread safe.
    /// All accesses (except first one) are lock-free.
    /// </summary>
    /// <typeparam name="TImplementation">Type implementing properties.</typeparam>
    /// <typeparam name="TDeclaration">Type declaring properties.</typeparam>
    public static class PropertyImplementation<TImplementation,TDeclaration> where TImplementation:TDeclaration
    {
// ReSharper disable StaticFieldInGenericType
        private static volatile Dictionary<String, Delegate> _getters;
        private static volatile Dictionary<String, Delegate> _setters;
        private static readonly Object SyncRoot = new object();
// ReSharper restore StaticFieldInGenericType

        private static Dictionary<String, Delegate> Getters
        {
            get
            {
                if (_getters == null)
                {
                    lock (SyncRoot)
                    {
                        if (_getters == null)
                        {
                            _getters = CompileGetters();
                        }
                    }
                }
                return _getters;
            }
        }

        private static Dictionary<String, Delegate> Setters
        {
            get
            {
                if (_setters == null)
                {
                    lock (SyncRoot)
                    {
                        if (_setters == null)
                        {
                            _setters = CompileSetters();
                        }
                    }
                }
                return _setters;
            }
        }

        /// <summary>
        /// Returns delegate calling an implementation of property propertyName getter declared in TDeclaration, defined in TImplementation.
        /// </summary>
        /// <typeparam name="TValue">Property type.</typeparam>
        /// <param name="propertyName">Name of property.</param>
        /// <returns>Delegate, calling specified function on given instance.</returns>
        public static Func<TImplementation, TValue> GetGetter<TValue>(string propertyName)
        {
            return (Func<TImplementation, TValue>)Getters[propertyName];
        }

        /// <summary>
        /// Returns delegate calling an implementation of property propertyName setter declared in TDeclaration, defined in TImplementation.
        /// </summary>
        /// <typeparam name="TValue">Property type.</typeparam>
        /// <param name="propertyName">Name of property.</param>
        /// <returns>Delegate, calling specified function on given instance.</returns>
        public static Action<TImplementation, TValue> GetSetter<TValue>(string propertyName)
        {
            return (Action<TImplementation, TValue>)Setters[propertyName];
        }

        public static TValue Get<TValue>(TImplementation instance, string propertyName)
        {
            return GetGetter<TValue>(propertyName)(instance);
        }

        public static void Set<TValue>(TImplementation instance, string propertyName, TValue value)
        {
            GetSetter<TValue>(propertyName)(instance, value);
        }

        private static MethodInfo GetBaseDefinition(MethodInfo method)
        {
            MethodInfo baseDefinition;
            do
            {
                baseDefinition = method.GetBaseDefinition();
            } while (baseDefinition.GetBaseDefinition() != baseDefinition);
            return baseDefinition;
        }


        private static Dictionary<String, Delegate> CompileGetters()
        {
            var implementationType = typeof(TImplementation);
            var declarationType = typeof (TDeclaration);

            if(implementationType.IsInterface)
            {
                throw new ArgumentException("TImplementation cannot be an interface.");
            }

            if(!declarationType.IsAssignableFrom(implementationType))
            {
                throw new ArgumentException("ImplementingType is not assignable from InstanceType");
            }

            var propertys =
                declarationType.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance);
            var getters = new Dictionary<String, Delegate>(propertys.Length);

            foreach (var property in propertys)
            {
                if (property.CanRead)
                {
                    MethodInfo method = null;
                    if (!declarationType.IsInterface)
                    {
                        method = property.GetGetMethod();

                        if(method.IsVirtual && !method.IsFinal)
                        {
                            var baseDefinition = GetBaseDefinition(method);
                            

                            foreach (var methodInfo in implementationType.GetMethods(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance))
                            {
                                var foundBaseDefinition = GetBaseDefinition(methodInfo);
                                if(baseDefinition == foundBaseDefinition)
                                {
                                    method = methodInfo;
                                    break;
                                }
                            }
                        }
                    }
                    else
                    {
                        var mapping = implementationType.GetInterfaceMap(declarationType);
                        for (int i = 0; i < mapping.InterfaceMethods.Length; i++ )
                        {
                            if (mapping.InterfaceMethods[i] == property.GetGetMethod())
                            {
                                method = mapping.TargetMethods[i];
                                break;
                            }
                        }
                    }
                    var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();
                    
                    var dynamicmethod = new DynamicMethod(method.Name,method.ReturnType, new Type[]{implementationType},implementationType,true);

                    var il = dynamicmethod.GetILGenerator();

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Tailcall);
                    il.Emit(OpCodes.Call, method);
                    il.Emit(OpCodes.Ret);
                    
                    var @delegate = dynamicmethod.CreateDelegate(typeof (Func<,>).MakeGenericType(implementationType,method.ReturnType));
                    getters.Add(property.Name,@delegate);
                }
            }
            return getters;
        }

        private static Dictionary<String, Delegate> CompileSetters()
        {
            var implementationType = typeof(TImplementation);
            var declaringType = typeof(TDeclaration);

            if (!declaringType.IsAssignableFrom(implementationType))
            {
                throw new ArgumentException("ImplementingType is not assignable from InstanceType");
            }

            var propertys = declaringType.GetProperties(BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance);
            var setters = new Dictionary<String, Delegate>(propertys.Length);

            foreach (var property in propertys)
            {
                if (property.CanWrite)
                {
                    MethodInfo method = null;
                    if (!declaringType.IsInterface)
                    {
                        method = property.GetSetMethod();

                        if (method.IsVirtual && !method.IsFinal)
                        {
                            var baseDefinition = GetBaseDefinition(method);


                            foreach (var methodInfo in implementationType.GetMethods(BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance))
                            {
                                var foundBaseDefinition = GetBaseDefinition(methodInfo);

                                if (baseDefinition == foundBaseDefinition)
                                {
                                    method = methodInfo;
                                    break;
                                }
                            }
                        }
                    }
                    else
                    {
                        var mapping = implementationType.GetInterfaceMap(declaringType);
                        for (int i = 0; i < mapping.InterfaceMethods.Length; i++)
                        {
                            if (mapping.InterfaceMethods[i] == property.GetSetMethod())
                            {
                                method = mapping.TargetMethods[i];
                                break;
                            }
                        }
                    }
                    var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();

                    var dynamicmethod = new DynamicMethod(method.Name, typeof(void), new Type[] { implementationType,parameterTypes[0] }, implementationType,true);

                    var il = dynamicmethod.GetILGenerator();
                    

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldarg_1);
                    il.Emit(OpCodes.Tailcall);
                    il.Emit(OpCodes.Call, method);
                    il.Emit(OpCodes.Ret);

                    var @delegate = dynamicmethod.CreateDelegate(typeof(Action<,>).MakeGenericType(implementationType, parameterTypes[0]));
                    setters.Add(property.Name, @delegate);
                }
            }
            return setters;
        }
    }
}
